/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/
package chconfig

import (
	reqContext "context"
	"math/rand"

	"git.cloud.inspur.com/ichain/ichain-protos-go/common"
	mb "git.cloud.inspur.com/ichain/ichain-protos-go/iam"
	pb "git.cloud.inspur.com/ichain/ichain-protos-go/inode"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/errors/retry"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/verifier"
	contextImpl "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/ledger"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/resource"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/thirdparty/ichain/common/chainconfig"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/thirdparty/ichain/protoutil"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/thirdparty/ichain/security/iam"
	"github.com/golang/protobuf/proto"
	"github.com/pkg/errors"
)

// ChainConfigService 链配置服务
type ChainConfigService struct {
	chainID string
	opts    Opts
}

func NewChainConfigService(chainID string, options ...Option) (*ChainConfigService, error) {
	opts, err := prepareOpts(options...)
	if err != nil {
		return nil, err
	}

	return &ChainConfigService{chainID: chainID, opts: opts}, nil
}

func (c *ChainConfigService) QueryBlock(reqCtx reqContext.Context) (*common.Block, error) {

	if c.opts.Consenser != nil {
		return c.queryBlockFromConsenser(reqCtx)
	}

	return c.queryBlockFromInodes(reqCtx)
}

func (c *ChainConfigService) Query(reqCtx reqContext.Context) (ichain.ChainCfg, error) {

	if c.opts.Consenser != nil {
		return c.queryConsenser(reqCtx)
	}

	return c.queryInodes(reqCtx)
}

func (c *ChainConfigService) queryInodes(reqCtx reqContext.Context) (*ChainCfg, error) {
	block, err := c.queryBlockFromInodes(reqCtx)

	if err != nil {
		return nil, errors.WithMessage(err, "QueryBlockConfig failed")
	}
	return extractConfig(c.chainID, block)

}

func (c *ChainConfigService) queryBlockFromInodes(reqCtx reqContext.Context) (*common.Block, error) {
	ctx, ok := contextImpl.RequestClientContext(reqCtx)
	if !ok {
		return nil, errors.New("failed get client context from reqContext for signPayload")
	}

	l, err := ledger.NewLedger(c.chainID)
	if err != nil {
		return nil, errors.WithMessage(err, "ledger client creation failed")
	}

	c.resolveOptsFromConfig(ctx)

	targets := []ichain.Inode{}
	if c.opts.Targets == nil {
		targets, err = c.calculateTargetsFromConfig(ctx)
		if err != nil {
			return nil, err
		}
	} else {
		targets = c.opts.Targets
	}

	retryHandler := retry.New(c.opts.RetryOpts)

	block, err := retry.NewInvoker(retryHandler).Invoke(
		func() (interface{}, error) {
			return l.QueryConfigBlock(reqCtx, targets, &verifier.TransactionProposalResponseVerifier{MinResponses: c.opts.MinResponses})
		},
	)

	if err != nil {
		return nil, errors.WithMessage(err, "QueryBlockConfig failed")
	}
	return block.(*common.Block), nil

}

func (c *ChainConfigService) calculateTargetsFromConfig(ctx context.Client) ([]ichain.Inode, error) {
	var targets []ichain.Inode
	chInodes := ctx.EndpointConfig().ChainInodes(c.chainID)
	if len(chInodes) == 0 {
		return nil, errors.Errorf("no chain inodes configured for chain [%s]", c.chainID)
	}

	for _, p := range chInodes {
		newInode, err := ctx.IChainProvider().CreateInodeFromConfig(&p.NetworkInode)
		if err != nil || newInode == nil {
			return nil, errors.WithMessage(err, "NewInode failed")
		}

		if newInode.IAMID() == ctx.Identifier().IAMID {
			targets = append(targets, newInode)
		}
	}

	targets = randomMaxTargets(targets, c.opts.MaxTargets)
	return targets, nil
}

func (c *ChainConfigService) queryConsenser(reqCtx reqContext.Context) (*ChainCfg, error) {

	block, err := c.queryBlockFromConsenser(reqCtx)
	if err != nil {
		return nil, errors.WithMessage(err, "LastConfigFromConsenser failed")
	}

	return extractConfig(c.chainID, block)
}

func (c *ChainConfigService) queryBlockFromConsenser(reqCtx reqContext.Context) (*common.Block, error) {

	return resource.LastConfigFromConsenser(reqCtx, c.chainID, c.opts.Consenser, resource.WithRetry(c.opts.RetryOpts))
}

func (c *ChainConfigService) resolveOptsFromConfig(ctx context.Client) {

	if c.opts.MaxTargets != 0 && c.opts.MinResponses != 0 && c.opts.RetryOpts.RetryableCodes != nil {
		//already loaded
		return
	}

	chSdkCfg := ctx.EndpointConfig().ChainConfig(c.chainID)

	c.resolveMaxResponsesOptsFromConfig(chSdkCfg)
	c.resolveMinResponsesOptsFromConfig(chSdkCfg)
	c.resolveRetryOptsFromConfig(chSdkCfg)

	c.applyDefaultOpts()

}

func (c *ChainConfigService) resolveMaxResponsesOptsFromConfig(chSdkCfg *ichain.ChainEndpointConfig) {
	if c.opts.MaxTargets == 0 {
		c.opts.MaxTargets = chSdkCfg.Policies.QueryChainConfig.MaxTargets
	}
}

func (c *ChainConfigService) resolveMinResponsesOptsFromConfig(chSdkCfg *ichain.ChainEndpointConfig) {
	if c.opts.MinResponses == 0 {
		c.opts.MinResponses = chSdkCfg.Policies.QueryChainConfig.MinResponses
	}
}

func (c *ChainConfigService) resolveRetryOptsFromConfig(chSdkCfg *ichain.ChainEndpointConfig) {
	if c.opts.RetryOpts.RetryableCodes == nil {
		c.opts.RetryOpts = chSdkCfg.Policies.QueryChainConfig.RetryOpts
		c.opts.RetryOpts.RetryableCodes = retry.ChainConfigRetryableCodes
	}
}

func (c *ChainConfigService) applyDefaultOpts() {
	if c.opts.RetryOpts.Attempts == 0 {
		c.opts.RetryOpts.Attempts = retry.DefaultAttempts
	}

	if c.opts.RetryOpts.InitialBackoff == 0 {
		c.opts.RetryOpts.InitialBackoff = retry.DefaultInitialBackoff
	}

	if c.opts.RetryOpts.BackoffFactor == 0 {
		c.opts.RetryOpts.BackoffFactor = retry.DefaultBackoffFactor
	}

	if c.opts.RetryOpts.MaxBackoff == 0 {
		c.opts.RetryOpts.MaxBackoff = retry.DefaultMaxBackoff
	}

	if c.opts.RetryOpts.RetryableCodes == nil {
		c.opts.RetryOpts.RetryableCodes = retry.ChainConfigRetryableCodes
	}
}

func WithInodes(inodes []ichain.Inode) Option {
	return func(opts *Opts) error {
		opts.Targets = inodes
		return nil
	}
}

func WithMinResponses(min int) Option {
	return func(opts *Opts) error {
		opts.MinResponses = min
		return nil
	}
}

func WithConsenser(consenser ichain.Consenser) Option {
	return func(opts *Opts) error {
		opts.Consenser = consenser
		return nil
	}
}

func WithMaxTargets(maxTargets int) Option {
	return func(opts *Opts) error {
		opts.MaxTargets = maxTargets
		return nil
	}
}

func WithRetryOpts(retryOpts retry.Opts) Option {
	return func(opts *Opts) error {
		opts.RetryOpts = retryOpts
		return nil
	}
}

func prepareOpts(options ...Option) (Opts, error) {
	opts := Opts{}
	for _, option := range options {
		err := option(&opts)
		if err != nil {
			return opts, errors.WithMessage(err, "Failed to read query config opts")
		}
	}

	return opts, nil
}

func extractConfig(chainID string, block *common.Block) (*ChainCfg, error) {
	if block.Header == nil {
		return nil, errors.New("expected header in block")
	}

	configEnvelope, err := protoutil.CreateConfigEnvelope(block.Data.Data[0])
	if err != nil {
		return nil, err
	}

	group := configEnvelope.Config.ChainGroup

	versions := &ichain.Versions{
		Chain: &common.ConfigGroup{},
	}

	config := &ChainCfg{
		id:           chainID,
		blockNumber:  block.Header.Number,
		iams:         []*mb.IAMConfig{},
		anchorInodes: []*ichain.OrgAnchorInode{},
		consensers:   []string{},
		versions:     versions,
		capabilities: make(map[ichain.ConfigGroupKey]map[string]bool),
	}

	err = loadConfig(config, config.versions.Chain, group, "", "")
	if err != nil {
		return nil, errors.WithMessage(err, "load config items from config group failed")
	}

	logger.Debugf("loaded chain config: %+v", config)

	return config, err

}

func loadConfig(configItems *ChainCfg, versionsGroup *common.ConfigGroup, group *common.ConfigGroup, name string, org string) error {
	if group == nil {
		return nil
	}

	versionsGroup.Version = group.Version
	versionsGroup.ModPolicy = group.ModPolicy

	groups := group.GetGroups()
	if groups != nil {
		versionsGroup.Groups = make(map[string]*common.ConfigGroup)
		for key, configGroup := range groups {
			logger.Debugf("loadConfigGroup - %s - found config group ==> %s", name, key)
			// The Application group is where config settings are that we want to find
			versionsGroup.Groups[key] = &common.ConfigGroup{}
			err := loadConfig(configItems, versionsGroup.Groups[key], configGroup, key, key)
			if err != nil {
				return err
			}
		}
	}

	values := group.GetValues()
	if values != nil {
		versionsGroup.Values = make(map[string]*common.ConfigValue)
		for key, configValue := range values {
			versionsGroup.Values[key] = &common.ConfigValue{}
			err := loadConfigValue(configItems, key, versionsGroup.Values[key], configValue, name, org)
			if err != nil {
				return err
			}

		}
	}

	loadConfigGroupPolicies(versionsGroup, group)

	return nil
}

func loadConfigGroupPolicies(versionsGroup *common.ConfigGroup, group *common.ConfigGroup) {
	policies := group.GetPolicies()
	if policies != nil {
		versionsGroup.Policies = make(map[string]*common.ConfigPolicy)
		for key, configPolicy := range policies {
			versionsGroup.Policies[key] = &common.ConfigPolicy{}

			versionsGroup.Policies[key].Version = configPolicy.Version
			versionsGroup.Policies[key].Policy = configPolicy.Policy
			versionsGroup.Policies[key].ModPolicy = configPolicy.ModPolicy
		}
	}
}

func loadAnchorInodes(configValue *common.ConfigValue, configItems *ChainCfg, org string) error {
	anchorInodes := &pb.AnchorInodes{}
	err := proto.Unmarshal(configValue.Value, anchorInodes)
	if err != nil {
		return errors.Wrap(err, "unmarshal anchor inodes from config failed")
	}

	if len(anchorInodes.AnchorInodes) > 0 {
		for _, anchorInode := range anchorInodes.AnchorInodes {
			oap := &ichain.OrgAnchorInode{Org: org, Host: anchorInode.Host, Port: anchorInode.Port}
			configItems.anchorInodes = append(configItems.anchorInodes, oap)
		}
	}
	return nil
}

func loadIAMKey(configValue *common.ConfigValue, configItems *ChainCfg) error {
	iamConfig := &mb.IAMConfig{}
	err := proto.Unmarshal(configValue.Value, iamConfig)
	if err != nil {
		return errors.Wrap(err, "unmarshal IAMConfig from config failed")
	}

	iamType := iam.ProviderType(iamConfig.Type)
	if iamType != iam.ICHAIN {
		return errors.Errorf("unsupported IAM type (%v)", iamType)
	}

	configItems.iams = append(configItems.iams, iamConfig)
	return nil

}

func loadConsenserAddressesKey(configValue *common.ConfigValue, configItems *ChainCfg) error {
	consenserAddresses := &common.ConsenserAddresses{}
	err := proto.Unmarshal(configValue.Value, consenserAddresses)
	if err != nil {
		return errors.Wrap(err, "unmarshal consenser addresses from config failed")
	}
	if len(consenserAddresses.Addresses) > 0 {
		configItems.consensers = append(configItems.consensers, consenserAddresses.Addresses...)
	}
	return nil

}

func loadCapabilities(configValue *common.ConfigValue, configItems *ChainCfg, groupName string) error {
	capabilities := &common.Capabilities{}
	err := proto.Unmarshal(configValue.Value, capabilities)
	if err != nil {
		return errors.Wrap(err, "unmarshal capabilities from config failed")
	}
	capabilityMap := make(map[string]bool)
	for capability := range capabilities.Capabilities {
		capabilityMap[capability] = true
	}
	configItems.capabilities[ichain.ConfigGroupKey(groupName)] = capabilityMap
	return nil
}

func loadConfigValue(configItems *ChainCfg, key string, versionsValue *common.ConfigValue, configValue *common.ConfigValue, groupName string, org string) error {
	versionsValue.Version = configValue.Version
	versionsValue.Value = configValue.Value

	switch key {
	case chainconfig.AnchorINodesKey:
		if err := loadAnchorInodes(configValue, configItems, org); err != nil {
			return err
		}
	case chainconfig.IAMKey:
		if err := loadIAMKey(configValue, configItems); err != nil {
			return err
		}
	case chainconfig.CapabilitiesKey:
		if err := loadCapabilities(configValue, configItems, groupName); err != nil {
			return err
		}
	//case chainConfig.ConsensusTypeKey:
	//	consensusType := &ab.ConsensusType{}
	//	err := proto.Unmarshal(configValue.Value, consensusType)
	//	if err != nil {
	//		return errors.Wrap(err, "unmarshal ConsensusType from config failed")
	//	}
	//
	//	logger.Debugf("loadConfigValue - %s   - Consensus type value :: %s", groupName, consensusType.Type)
	//	// TODO: Do something with this value
	//case chainConfig.BatchSizeKey:
	//	batchSize := &ab.BatchSize{}
	//	err := proto.Unmarshal(configValue.Value, batchSize)
	//	if err != nil {
	//		return errors.Wrap(err, "unmarshal batch size from config failed")
	//	}
	//
	//	// TODO: Do something with this value

	//case chainConfig.BatchTimeoutKey:
	//	batchTimeout := &ab.BatchTimeout{}
	//	err := proto.Unmarshal(configValue.Value, batchTimeout)
	//	if err != nil {
	//		return errors.Wrap(err, "unmarshal batch timeout from config failed")
	//	}
	//	// TODO: Do something with this value

	//case chainConfig.ChainRestrictionsKey:
	//	chainRestrictions := &ab.ChainRestrictions{}
	//	err := proto.Unmarshal(configValue.Value, chainRestrictions)
	//	if err != nil {
	//		return errors.Wrap(err, "unmarshal chain restrictions from config failed")
	//	}
	//	// TODO: Do something with this value

	//case chainConfig.HashingAlgorithmKey:
	//	hashingAlgorithm := &common.HashingAlgorithm{}
	//	err := proto.Unmarshal(configValue.Value, hashingAlgorithm)
	//	if err != nil {
	//		return errors.Wrap(err, "unmarshal hashing algorithm from config failed")
	//	}
	//	// TODO: Do something with this value

	//case chainConfig.ConsortiumKey:
	//	consortium := &common.Consortium{}
	//	err := proto.Unmarshal(configValue.Value, consortium)
	//	if err != nil {
	//		return errors.Wrap(err, "unmarshal consortium from config failed")
	//	}
	//	// TODO: Do something with this value

	//case chainConfig.BlockDataHashingStructureKey:
	//	bdhstruct := &common.BlockDataHashingStructure{}
	//	err := proto.Unmarshal(configValue.Value, bdhstruct)
	//	if err != nil {
	//		return errors.Wrap(err, "unmarshal block data hashing structure from config failed")
	//	}
	//	// TODO: Do something with this value

	case chainconfig.ConsenserAddressesKey:
		if err := loadConsenserAddressesKey(configValue, configItems); err != nil {
			return err
		}

	default:
	}
	return nil
}

func randomMaxTargets(targets []ichain.Inode, max int) []ichain.Inode {
	if len(targets) <= max {
		return targets
	}
	for i := range targets {
		j := rand.Intn(i + 1)
		targets[i], targets[j] = targets[j], targets[i]
	}
	return targets[:max]
}

type Opts struct {
	Consenser    ichain.Consenser
	Targets      []ichain.Inode
	MinResponses int
	MaxTargets   int
	RetryOpts    retry.Opts
}

type Option func(opts *Opts) error
